using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Web;


namespace DeathByCaptcha
{
    public class Client
    {
        public const string InvalidCredentialsError = "login.invalid_userpwd";

        public const string IsBannedError          = "user.is_banned";
        public const string NoFactoryError         = "user.no_factory_assigned";
        public const string InsufficientFundsError = "user.insufficient_funds";

        public const string CaptchaNotFoundError = "captcha.not_found";
        public const string CaptchaUploadError   = "captcha.upload_failed";
        public const string InvalidCaptchaError  = "captcha.invalid_data";


        private ApiCaller caller = new ApiCaller();
        private Hashtable userpwd = new Hashtable();

        public bool IsLoggedIn
        {
            get
            {
                return userpwd.ContainsKey("username") &&
                       userpwd.ContainsKey("password");
            }
            set
            {
                throw new AccessViolationException(
                    "Setting logged in/out flag is not allowed"
                );
            }
        }

        public Double Balance
        {
            get
            {
                if (this.IsLoggedIn) {
                    string key = "balance";
                    if (this.caller.Call(
                        "user",
                        this.GetEncodedCredentials()
                    ) && caller.Response.ContainsKey(key)) {
                        try {
                            return Double.Parse((string)caller.Response[key]);
                        } catch (Exception) {}
                    }
                }
                return 0.0;
            }
            set
            {
                throw new AccessViolationException(
                    "Balance property is read-only"
                );
            }
        }

        public Hashtable Response
        {
            get
            {
                return caller.Response;
            }
            set
            {
                this.caller.Response = value;
            }
        }

        public string Error
        {
            get
            {
                return caller.Error;
            }
            set
            {
                this.caller.Error = value;
            }
        }


        public int PollInterval
        {
            get
            {
                return caller.PollInterval;
            }
            set
            {
                caller.PollInterval = value;
            }
        }

        public WebProxy Proxy
        {
            get
            {
                return caller.Proxy;
            }
            set
            {
                caller.Proxy = value;
            }
        }


        private string GetEncodedCredentials()
        {
            return this.IsLoggedIn
                ? "username=" + HttpUtility.UrlEncode((string)this.userpwd["username"]) +
                  "&password=" + HttpUtility.UrlEncode((string)this.userpwd["password"]) +
                  "&is_hashed=1"
                : "";
        }


        public Client()
        {
            this.Init();
        }

        public Client Init()
        {
            this.caller.Init();
            this.userpwd.Clear();
            return this;
        }

        public bool Login(string username, string password)
        {
            this.userpwd.Clear();
            if (0 < username.Length) {
                this.userpwd.Add("username", username);
                if (0 < password.Length) {
                    SHA1 digest = new SHA1CryptoServiceProvider();
                    this.userpwd.Add("password", BitConverter.ToString(digest.ComputeHash(
                        Encoding.UTF8.GetBytes(password)
                    )).Replace("-", "").ToLower());
                    digest.Clear();
                }
            }
            return this.IsLoggedIn;
        }

        public Client Logout()
        {
            this.userpwd.Clear();
            return this;
        }

        public int Upload(byte[] image)
        {
            if (!this.IsLoggedIn) {
                return 0;
            }

            string crlf = "\r\n";
            string boundary = BitConverter.ToString(
                (new SHA1CryptoServiceProvider()).ComputeHash(
                    Encoding.UTF8.GetBytes(DateTime.Now.ToString("G"))
                )
            );
            byte[] body = Encoding.ASCII.GetBytes(
                "--" + boundary + crlf + "Content-Disposition: form-data; name=\"username\"" + crlf + crlf + (string)this.userpwd["username"] + crlf + crlf +
                "--" + boundary + crlf + "Content-Disposition: form-data; name=\"password\"" + crlf + crlf + (string)this.userpwd["password"] + crlf + crlf +
                "--" + boundary + crlf + "Content-Disposition: form-data; name=\"is_hashed\"" + crlf + crlf + "1" + crlf + crlf +
                "--" + boundary + crlf + "Content-Disposition: form-data; name=\"captchafile\"; filename=\"captcha\"" + crlf + "Content-Type: application/octet-stream" + crlf + crlf
            );

            int l = body.Length;
            Array.Resize(ref body, l + image.Length);
            image.CopyTo(body, l);

            byte[] footer = Encoding.ASCII.GetBytes(crlf + "--" + boundary + "--" + crlf);
            l = body.Length;
            Array.Resize(ref body, l + footer.Length);
            footer.CopyTo(body, l);

            if (this.caller.Call(
                "captcha",
                body,
                "multipart/form-data; boundary=" + boundary
            ) && this.Response.ContainsKey("captcha")) {
                if (this.Response.ContainsKey("poll_interval")) {
                    try {
                        this.PollInterval = Int16.Parse((string)this.Response["poll_interval"]);
                    } catch (Exception) {}
                }

                try {
                    return Int32.Parse((string)this.Response["captcha"]);
                } catch (Exception) {}
            }

            return 0;
        }

        public int Upload(string filename)
        {
            if (File.Exists(filename)) {
                return this.Upload(File.ReadAllBytes(filename));
            } else {
                throw new ArgumentException(
                    "Image file " + filename + " not found"
                );
            }
        }

        public string GetText(int id, int maxPolls)
        {
            string method = "captcha/" + id;

            int poll = 0;
            while ((0 == maxPolls) || (poll < maxPolls)) {
                if (this.Response.ContainsKey("text")) {
                    string text = (string)this.Response["text"];
                    if ("" != text) {
                        return text;
                    }
                }

                Thread.Sleep(this.PollInterval * 1000);
                poll++;

                this.caller.Call(method);
            }

            return "";
        }

        public string GetText(int id)
        {
            return this.GetText(id, 0);
        }

        public bool Remove(int id)
        {
            return this.IsLoggedIn &&
                   this.caller.Call("captcha/" + id + "/remove", this.GetEncodedCredentials());
        }

        public bool Report(int id, bool isCorrect)
        {
            return isCorrect ||
                   (this.IsLoggedIn && this.caller.Call("captcha/" + id + "/report", this.GetEncodedCredentials()));
        }

        public bool Report(int id)
        {
            return this.Report(id, true);
        }

        public string Decode(string filename, int maxPolls)
        {
            int id = this.Upload(filename);
            if (0 < id) {
                string text = this.GetText(id, maxPolls);
                if ("" != text) {
                    return text;
                } else {
                    this.Remove(id);
                }
            }
            return "";
        }

        public string Decode(string filename)
        {
            return this.Decode(filename, 0);
        }
    }
}
